package de.uni_stuttgart.ipvs.ids.nameserver;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.HashMap;

/**
 * Nameserver to resolve names to addresses using the recursive or iterative method.
 * @author Team 3/4 - Elkhan Dadashov, Dhaval A Shah, Patrick Stortz
 */
public class Nameserver {
	/** The host address the name server is running on. */
	private final static String IP = "localhost";
	/** The name identifying this instance */
	private String mName;
	/** The domain handled by this Nameserver instance. */
	private String domain;
	
	/** Reference to the parent Nameserver instance. */
	private Nameserver parent;
	/** The children of this name server instance. Key is the domain and Value is the corresponding Nameserver instance. */
	private HashMap<String, Nameserver> children;
	/** The saved names of this name server instance. Key is the name and Value is the corresponding address. */
	private HashMap<String, String> savedNames;

	/** Reference to the TCPListener to be used */
	private TCPListener tcpListener;
	/** The port for the TCPListener (TCP socket) to bind to. */
	private int tcpPort;
	/** Reference to the UDPListener to be used */
	private UDPListener udpListener;
	/** The port for the UDPListener (UDP socket) to bind to. */
	private int udpPort;

	/** The timeout in milliseconds for socket connect() and receive() calls. */
	private static int TIMEOUT = 5000;
	/** A regular expression for checking domain names */
	private final static String DOMAINREGEX = "^[a-z0-9]+([\\-\\.]{1}[a-z0-9]+)*\\.[a-z]{2,5}$";

	/**
	 * @return the name identifying this instance.
	 */
	public String getName() {
		return this.mName;
	}

	/**
	 * Creates a Nameserver instance. Starts the TCPListener and the UDPListener to receive requests (recursive or iterative).
	 * @param domain the domain handled by this name server
	 * @param parent the parent Nameserver of this one
	 * @param tcpPort the port used for the TCPListener
	 * @param udpPort the port used for the UDPListener
	 */
	public Nameserver(String domain, Nameserver parent, int tcpPort, int udpPort) {
		this.children = new HashMap<String, Nameserver>();
		this.savedNames = new HashMap<String, String>();
		this.domain = domain;
		this.parent = parent;
		this.tcpPort = tcpPort;
		this.udpPort = udpPort;		
		this.mName = "Nameserver " + domain;

		try {
			// try to get the IP address of this machine
			InetAddress ip = InetAddress.getLocalHost();
			this.mName = this.mName + " @ " + ip.getHostAddress();
		} catch (UnknownHostException e) {
			// ignore if we are not able to get the IP
		}
		
		this.tcpListener = new TCPListener(this, tcpPort);
		this.udpListener = new UDPListener(this, udpPort);
		this.tcpListener.start();
		this.udpListener.start();
	}

	/**
	 * Add a child to this Nameserver.
	 * @param domain the domain of the child name server
	 * @param nameserver the child name server
	 */
	public void addChild(String domain, Nameserver nameserver) {
		System.out.println(getName() + " | Added " + nameserver.getName() + " as child.");// DEBUG LOG OUTPUT
		this.children.put(domain, nameserver);
	}

	/**
	 * Add a name-address-pair to this Nameserver
	 * @param name the name
	 * @param address the corresponding resolved address
	 */
	public void addNameAddressPair(String name, String address) {
		System.out.println(getName() + " | Added (" + name + "," + address + ") to its list.");// DEBUG LOG OUTPUT
		this.savedNames.put(name, address);
	}

	/** @return The port for the UDPListener (UDP socket) to bind to. */
	public int getUdpPort() {
		return this.udpPort;
	}

	/** @return The port for the TCPListener (TCP socket) to bind to. */
	public int getTcpPort() {
		return this.tcpPort;
	}

	/** @return The host address the name server is running on. */
	public String getAddress() {
		return this.IP;
	}

	/** @return The domain handled by this Nameserver instance. */
	public String getDomain() {
		return this.domain;
	}

	/**
	 * Processes a recursive resolution request.
	 * @param parentSocket The TCP socket connection to the requesting component.
	 * @throws IOException In case the TCP socket communication to the component fails.
	 */
	public void processRecursiveRequest(Socket parentSocket) throws IOException{

		// get the streams of the parent server
		PrintWriter parentOutputWriter = new PrintWriter(parentSocket.getOutputStream(), true);
		BufferedReader parentInputReader = new BufferedReader(new InputStreamReader(parentSocket.getInputStream()));

		// read the request of the parent
		String name = parentInputReader.readLine().trim();
		System.out.println(getName() + " | Client at " + parentSocket.getRemoteSocketAddress() + " needs address for " + name);// DEBUG LOG OUTPUT

		// check if we already cached/know this name
		String address = this.savedNames.get(name);
		if(address!=null){
			// we already cached/know this address
			System.out.println(getName() + " | Found address " + address + " for name " + name + " in its list!");// DEBUG LOG OUTPUT
		}else{
			// check if we are the root server and if the request is valid
			if(this.parent==null && name.matches(DOMAINREGEX)==false){
				// the domain is not valid, inform the client
				System.out.println(getName() + " | Error! Name " + name + " is not a valid domain!");// DEBUG LOG OUTPUT
				address = "?.?.?.? (Invalid domain)";
			}else{
				try{
					// we don't know/haven't cached this address 
					String childDomain = null;
					// check if there are more than one domain left
					if(name.contains(".")){
						// more than one domain left, get the highest domain
						childDomain = name.substring(name.lastIndexOf(".")+1);
						// get our child server for this domain
						Nameserver childServer = this.children.get(childDomain);
						// connect to the child server
						Socket childSocket = new Socket();
						childSocket.connect(new InetSocketAddress(childServer.getAddress(), childServer.getTcpPort()), TIMEOUT);
						PrintWriter childOutputWriter = new PrintWriter(childSocket.getOutputStream(), true);
						BufferedReader childInputReader = new BufferedReader(new InputStreamReader(childSocket.getInputStream()));
						// remove the highest domain
						String childRequest = name.substring(0, name.lastIndexOf("."));
						// send request to the child server
						System.out.println(getName() + " | Address not known! Asking " + childServer.getName() + " for " + childRequest);// DEBUG LOG OUTPUT
						childOutputWriter.println(childRequest);
						// wait for the response of the child server
						address = childInputReader.readLine();
						// close the child socket and streams
						childOutputWriter.close();
						childInputReader.close();
						childSocket.close();
					} else {
						// there is only one domain left, has to be one of our child name servers
						childDomain = name;
						// get our child server for this domain
						Nameserver childServer = this.children.get(childDomain);
						// since this is the last domain, the child server is the searched name
						System.out.println(getName() + " | Found address " + address + " for name " + name + " - It is one of our child servers!");// DEBUG LOG OUTPUT	
						address = childServer.getAddress();
					}
				}catch(Exception e){
					// an error during processing the name resolution occurred
					System.out.println(getName() + " | Error! Name " + name + " cannot be resovled! (" + e.getMessage() + ")");// DEBUG LOG OUTPUT
					address = "?.?.?.? (Cannot be resolved by " + this.getAddress() + ":" + this.getTcpPort() + ")";
				}
			}
		}

		// send address to the requesting parent
		System.out.println(getName() + " | Forwarding requested address to requesting component at " + parentSocket.getRemoteSocketAddress());// DEBUG LOG OUTPUT
		parentOutputWriter.println(address);

		// close the parent socket and streams
		parentOutputWriter.close();
		parentInputReader.close();
		parentSocket.close();
	}

	/**
	 * Processes a iterative resolution request.
	 * @param requestPacket The UDP data packet of the requesting component.
	 * @throws IOException In case the UDP socket communication fails.
	 */
	public void processIterativRequest(DatagramPacket requestPacket) throws IOException  {

		// read the request of the client
		String name = new String(requestPacket.getData()).trim();
		System.out.println(getName() + " | Client at " + requestPacket.getAddress() + " needs address of " + name);// DEBUG LOG OUTPUT

		// check if we already know this name
		String address = this.savedNames.get(name);
		if(address!=null){
			// we already know this address
			System.out.println(getName() + " | Found address " + address + " for name " + name + "!");// DEBUG LOG OUTPUT	
		}else{
			// check if we are the root server and if the request is valid
			if(this.parent==null && name.matches(DOMAINREGEX)==false){
				// the domain is not valid, inform the client
				address = "?.?.?.? (Invalid domain)";
				System.out.println(getName() + " | Error! Name " + name + " is not a valid domain!");// DEBUG LOG OUTPUT
			}else
				try{
					// get the highest domain
					String childDomain = name.substring(name.lastIndexOf(".")+1);
					// get our child server for this domain
					Nameserver childServer = this.children.get(childDomain);
					// get the address of this child server
					address = childServer.getAddress();
					// check if there are more than one domain left
					if(name.contains(".")){
						// more than one domain left, add the port
						address = address + ":" + childServer.getUdpPort();
						System.out.println(getName() + " | Address not known! Replying address of next name server (" + childDomain + "@" + address + ") to client!");// DEBUG LOG OUTPUT	
					} else {
						System.out.println(getName() + " | Found address " + address + " for name " + name + " - It is one of our child servers!");// DEBUG LOG OUTPUT	
					}
				} catch(Exception e){
					// an error during processing the name resolution occurred
					System.out.println(getName() + " | Error! Name " + name + " cannot be resovled! (" + e.getMessage() + ")");// DEBUG LOG OUTPUT
					address = "?.?.?.? (Cannot be resolved by " + this.getAddress() + ":" + this.getUdpPort() + ")";
				}
		}

		// create the reply packet
		DatagramPacket replyPacket = new DatagramPacket(address.getBytes(), address.length());
		// set the address
		replyPacket.setAddress(requestPacket.getAddress());
		// set the port
		replyPacket.setPort(requestPacket.getPort());
		// create a socket
		DatagramSocket socket = new DatagramSocket();
		// send the packet
		socket.send(replyPacket);
		// close the socket
		socket.close();
	}
}
